// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © blackcat1402
//@version=6
indicator('[blackcat] L1 Banker Fund X', shorttitle = 'L1 Banker Fund X', overlay = true, max_labels_count = 500, max_lines_count = 500)

// -------------------------------------------------------------
// CONST group names (literal strings — required by const qualifiers)
// -------------------------------------------------------------
var string GRP_CORE = 'Fund Flow Engine'
var string GRP_SIG  = 'Signals'
var string GRP_RISK = 'Risk Management'
var string GRP_MTF  = 'HTF Direction Gate'
var string GRP_RE   = 'Re-entry'
var string GRP_VIS  = 'Visuals'

// -------------------------------------------------------------
// Core engine inputs (all former hard-coded constants exposed)
// -------------------------------------------------------------
slopeLen     = input.int(21, 'Regression Slope Length', minval = 2, group = GRP_CORE, tooltip = 'Length of the linear regression used by the slope term (was fixed 21).')
slopeGain    = input.float(20.0, 'Slope Gain', step = 1.0, group = GRP_CORE, tooltip = 'Multiplier applied to the regression slope before it is added to close (was fixed 20).')
buyLen       = input.int(3, 'Buy Line EMA Length', minval = 1, group = GRP_CORE, tooltip = 'EMA length of the fast / accumulation line (was fixed 3).')
sellLen      = input.int(55, 'Sell Line EMA Length', minval = 2, group = GRP_CORE, tooltip = 'EMA length of the slow / distribution line (was fixed 55).')
brickScale   = input.float(1.0, 'Brick Spread', minval = 0.1, maxval = 3.0, step = 0.05, group = GRP_CORE, tooltip = 'Scales the vertical spread of the fund-flow bricks. 1.0 = original geometry.')
showBricks   = input.bool(true, 'Show Fund Flow Bricks', group = GRP_CORE)
bullLeadCol  = input.color(color.aqua, 'Bullish Lead Brick', group = GRP_CORE, inline = 'bc1')
bullBodyCol  = input.color(color.blue, 'Bullish Body Bricks', group = GRP_CORE, inline = 'bc1')
bearLeadCol  = input.color(color.green, 'Bearish Lead Brick', group = GRP_CORE, inline = 'bc2')
bearBodyCol  = input.color(color.green, 'Bearish Body Bricks', group = GRP_CORE, inline = 'bc2')
bearTailCol  = input.color(color.lime, 'Bearish Tail Brick', group = GRP_CORE, inline = 'bc2')

// -------------------------------------------------------------
// Helpers
// -------------------------------------------------------------
// SLOPE(X, N): linear-regression slope of the last N bars, per bar of the chart timeframe.
xsl(src, len) =>
    out = 0.0
    lrc = ta.linreg(src, len, 0)
    lrprev = ta.linreg(src[1], len, 0)
    out := (lrc - lrprev) / timeframe.multiplier
    out

// -------------------------------------------------------------
// Core algorithm (unchanged from V1)
// -------------------------------------------------------------
buyLine  = ta.ema(close, buyLen)
sellLine = ta.ema(xsl(close, slopeLen) * slopeGain + close, sellLen)
tradeDiff = buyLine - sellLine

// Fund-flow state: positive = accumulation (buyers in control), negative = distribution.
bullFlow = showBricks and tradeDiff >= 0
bearFlow = showBricks and tradeDiff < 0

// -------------------------------------------------------------
// Core plots
// -------------------------------------------------------------
plot(sellLine, 'Sell Line (Distribution)', color = color.new(color.lime, 0), linewidth = 2)
plot(sellLine[1] >= sellLine ? sellLine : na, 'Sell Line Rising', color = color.new(color.fuchsia, 0), linewidth = 2)
plot(buyLine, 'Buy Line (Accumulation)', color = showBricks ? color.new(color.teal, 40) : na, linewidth = 1)

// -------------------------------------------------------------
// Fund-flow bricks (V1 geometry preserved, scaled by brickScale)
// Each plotcandle consumes 8 of the 64-plot budget, so bull and bear
// share one slot per brick level: 5 slots instead of 10.
// -------------------------------------------------------------
float s = brickScale
bool brickOn = bullFlow or bearFlow

// Brick geometry: top / bottom of each brick as a fraction of tradeDiff.
float top1 = bullFlow ? buyLine - tradeDiff * 0.05 * s : sellLine + tradeDiff * 0.05 * s
float bot1 = bullFlow ? buyLine - tradeDiff * 0.15 * s : sellLine + tradeDiff * 0.15 * s
float top2 = bullFlow ? buyLine - tradeDiff * 0.20 * s : sellLine + tradeDiff * 0.20 * s
float bot2 = bullFlow ? buyLine - tradeDiff * 0.35 * s : sellLine + tradeDiff * 0.35 * s
float top3 = bullFlow ? buyLine - tradeDiff * 0.40 * s : sellLine + tradeDiff * 0.40 * s
float bot3 = bullFlow ? buyLine - tradeDiff * 0.55 * s : sellLine + tradeDiff * 0.55 * s
float top4 = bullFlow ? buyLine - tradeDiff * 0.60 * s : sellLine + tradeDiff * 0.60 * s
float bot4 = bullFlow ? buyLine - tradeDiff * 0.75 * s : sellLine + tradeDiff * 0.75 * s
float top5 = bullFlow ? buyLine - tradeDiff * 0.80 * s : sellLine + tradeDiff * 0.80 * s
float bot5 = bullFlow ? buyLine - tradeDiff * 0.95 * s : sellLine + tradeDiff * 0.95 * s

color brickCol1 = bullFlow ? bullLeadCol : bearLeadCol
color brickCol2 = bullFlow ? bullBodyCol : bearBodyCol
color brickCol3 = bullFlow ? bullBodyCol : bearBodyCol
color brickCol4 = bullFlow ? bullBodyCol : bearBodyCol
color brickCol5 = bullFlow ? bullBodyCol : bearTailCol

plotcandle(brickOn ? top1 : na, brickOn ? top1 : na, brickOn ? bot1 : na, brickOn ? bot1 : na, title = 'Fund Flow Brick 1', color = brickCol1, wickcolor = brickCol1, bordercolor = brickCol1)
plotcandle(brickOn ? top2 : na, brickOn ? top2 : na, brickOn ? bot2 : na, brickOn ? bot2 : na, title = 'Fund Flow Brick 2', color = brickCol2, wickcolor = brickCol2, bordercolor = brickCol2)
plotcandle(brickOn ? top3 : na, brickOn ? top3 : na, brickOn ? bot3 : na, brickOn ? bot3 : na, title = 'Fund Flow Brick 3', color = brickCol3, wickcolor = brickCol3, bordercolor = brickCol3)
plotcandle(brickOn ? top4 : na, brickOn ? top4 : na, brickOn ? bot4 : na, brickOn ? bot4 : na, title = 'Fund Flow Brick 4', color = brickCol4, wickcolor = brickCol4, bordercolor = brickCol4)
plotcandle(brickOn ? top5 : na, brickOn ? top5 : na, brickOn ? bot5 : na, brickOn ? bot5 : na, title = 'Fund Flow Brick 5', color = brickCol5, wickcolor = brickCol5, bordercolor = brickCol5)

// -------------------------------------------------------------
// Signal inputs
// -------------------------------------------------------------
enableShort   = input.bool(true, 'Enable Short Signals', group = GRP_SIG, tooltip = 'Turn off to keep the original long-only behaviour.')
useSlopeGate  = input.bool(false, 'Require Sell-Line Slope Alignment', group = GRP_SIG, tooltip = 'Long entries need a rising Sell Line; short entries need a falling one.')
useExclusivity = input.bool(true, 'Same-bar Entry/Exit Exclusivity', group = GRP_SIG)
cooldownBars  = input.int(3, 'Cooldown Bars', minval = 0, group = GRP_SIG, tooltip = 'Minimum number of bars between two consecutive signals. 0 disables the filter.')
showPreview   = input.bool(true, 'Show Unconfirmed Preview', group = GRP_SIG, tooltip = 'Intrabar, repaintable early marker. Preview signals are labelled as unconfirmed.')

// -------------------------------------------------------------
// HTF direction gate (optional, zero-repaint)
// -------------------------------------------------------------
useMTF   = input.bool(false, 'Enable HTF Direction Gate', group = GRP_MTF)
htfTf    = input.timeframe('240', 'HTF Timeframe', group = GRP_MTF)
htfEmaLen = input.int(50, 'HTF EMA Length', minval = 1, group = GRP_MTF)

// HTF EMA evaluated on the higher timeframe, then offset by one HTF bar (no future leak).
f_htfEma(len) =>
    e = ta.ema(close, len)
    e[1]

htfOk    = timeframe.in_seconds(htfTf) > timeframe.in_seconds()
htfClose = request.security(syminfo.tickerid, htfTf, close[1], lookahead = barmerge.lookahead_on)
htfEma   = request.security(syminfo.tickerid, htfTf, f_htfEma(htfEmaLen), lookahead = barmerge.lookahead_on)
htfDirRaw = htfClose > htfEma ? 1 : htfClose < htfEma ? -1 : 0
htfDir   = useMTF and htfOk and not na(htfClose) and not na(htfEma) ? htfDirRaw : 0
longGate  = htfDir >= 0
shortGate = htfDir <= 0

// -------------------------------------------------------------
// Raw signal predicates (V1 core: Buy Line vs Sell Line)
// -------------------------------------------------------------
sellSlopeUp   = sellLine >= nz(sellLine[1], sellLine)
sellSlopeDown = sellLine < nz(sellLine[1], sellLine)

longCross  = ta.crossover(buyLine, sellLine)
shortCross = ta.crossunder(buyLine, sellLine)

longEntryCond  = longCross and (not useSlopeGate or sellSlopeUp) and longGate
shortEntryCond = shortCross and (not useSlopeGate or sellSlopeDown) and shortGate

// -------------------------------------------------------------
// Position state machine + cooldown + same-bar arbitration
// -------------------------------------------------------------
var int posState      = 0     // -1 short / 0 flat / +1 long
var int lastSignalBar = na

cooldownOK = na(lastSignalBar) or bar_index - lastSignalBar > cooldownBars

rawLongEntry  = longEntryCond and posState <= 0 and cooldownOK
rawShortEntry = shortEntryCond and enableShort and posState >= 0 and cooldownOK
rawLongExit   = shortCross and posState == 1
rawShortExit  = longCross and posState == -1

longExit   = useExclusivity ? rawLongExit and not rawLongEntry and not rawShortEntry : rawLongExit
shortExit  = useExclusivity ? rawShortExit and not rawLongEntry and not rawShortEntry : rawShortExit
longEntry  = useExclusivity ? rawLongEntry and not longExit and not shortExit : rawLongEntry
shortEntry = useExclusivity ? rawShortEntry and not longExit and not shortExit : rawShortEntry

// -------------------------------------------------------------
// Re-entry anchored on the exit bar (optional)
// -------------------------------------------------------------
useReentry  = input.bool(false, 'Enable Re-entry (RL / RS)', group = GRP_RE)
reentryWin  = input.int(3, 'Anchor Window (bars around exit)', minval = 1, group = GRP_RE)
reentryMode = input.string('Close', 'Breakout Source', options = ['Close', 'High/Low'], group = GRP_RE)

var float exitHighAnchor = na
var float exitLowAnchor  = na
var bool  reentryArmed   = false

exitEdge  = longExit or shortExit
entryEdge = longEntry or shortEntry

// ta.* called on every bar (consistency), consumed only on the exit edge.
anchorHigh = ta.highest(high, reentryWin * 2 + 1)
anchorLow  = ta.lowest(low, reentryWin * 2 + 1)

if exitEdge
    exitHighAnchor := anchorHigh
    exitLowAnchor  := anchorLow
    reentryArmed   := true
if entryEdge
    reentryArmed := false

breakUpSrc   = reentryMode == 'Close' ? close > nz(exitHighAnchor, close) : high > nz(exitHighAnchor, high)
breakDownSrc = reentryMode == 'Close' ? close < nz(exitLowAnchor, close) : low < nz(exitLowAnchor, low)

RL = useReentry and reentryArmed and posState == 0 and cooldownOK and breakUpSrc and not breakDownSrc and longGate
RS = useReentry and enableShort and reentryArmed and posState == 0 and cooldownOK and breakDownSrc and not breakUpSrc and shortGate

if RL
    posState := 1
    lastSignalBar := bar_index
else if RS
    posState := -1
    lastSignalBar := bar_index
else if longEntry
    posState := 1
    lastSignalBar := bar_index
else if shortEntry
    posState := -1
    lastSignalBar := bar_index
else if longExit or shortExit
    posState := 0
    lastSignalBar := bar_index

plot(posState, 'Position State (-1 short / 0 flat / +1 long)', display = display.data_window)

// -------------------------------------------------------------
// Risk management: ATR-based take-profit / stop-loss
// -------------------------------------------------------------
useTPSL = input.bool(true, 'Show TP / SL Levels', group = GRP_RISK)
atrLen  = input.int(14, 'ATR Length', minval = 1, group = GRP_RISK)
slMult  = input.float(2.0, 'Stop Loss (xATR)', minval = 0.5, step = 0.25, group = GRP_RISK)
tpMult  = input.float(3.0, 'Take Profit (xATR)', minval = 0.5, step = 0.25, group = GRP_RISK)
useTrail = input.bool(false, 'Use Chandelier Trailing Stop', group = GRP_RISK)
trailMult = input.float(3.0, 'Trail Distance (xATR)', minval = 0.5, step = 0.25, group = GRP_RISK)

atrVal = ta.atr(atrLen)

var float entryPrice = na
var int   entryDir   = 0
var float extreme    = na

if longEntry or RL
    entryPrice := close
    entryDir   := 1
    extreme    := high
else if shortEntry or RS
    entryPrice := close
    entryDir   := -1
    extreme    := low
else if longExit or shortExit
    entryPrice := na
    entryDir   := 0
    extreme    := na
else if posState == 1
    extreme := math.max(nz(extreme, high), high)
else if posState == -1
    extreme := math.min(nz(extreme, low), low)

float fixedSL = useTPSL and not na(entryPrice) ? entryPrice - entryDir * slMult * atrVal : na
float fixedTP = useTPSL and not na(entryPrice) ? entryPrice + entryDir * tpMult * atrVal : na
float trailSL = useTPSL and useTrail and not na(extreme) ? extreme - entryDir * trailMult * atrVal : na

float slPrice = useTrail ? trailSL : fixedSL
float tpPrice = fixedTP

plot(slPrice, 'Stop Loss', color = color.new(color.red, 20), style = plot.style_linebr, linewidth = 2)
plot(tpPrice, 'Take Profit', color = color.new(color.teal, 20), style = plot.style_linebr, linewidth = 2)

// TP / SL hit detection (informational exit markers)
longTPHit  = posState == 1 and not na(tpPrice) and high >= tpPrice
longSLHit  = posState == 1 and not na(slPrice) and low <= slPrice
shortTPHit = posState == -1 and not na(tpPrice) and low <= tpPrice
shortSLHit = posState == -1 and not na(slPrice) and high >= slPrice
riskHit    = useTPSL and (longTPHit or longSLHit or shortTPHit or shortSLHit)

// -------------------------------------------------------------
// Unconfirmed intrabar preview (repaints until the bar closes)
// -------------------------------------------------------------
previewLong  = showPreview and not barstate.isconfirmed and posState <= 0 and longCross
previewShort = showPreview and not barstate.isconfirmed and enableShort and posState >= 0 and shortCross

// -------------------------------------------------------------
// Visuals
// -------------------------------------------------------------
showLabels = input.bool(true, 'Show Signal Labels', group = GRP_VIS)
showBg     = input.bool(true, 'Colour Background by Position', group = GRP_VIS)
showDash   = input.bool(true, 'Show Dashboard', group = GRP_VIS)

color C_LONG    = color.new(color.green, 0)
color C_SHORT   = color.new(color.red, 0)
color C_EXIT    = color.new(color.orange, 0)
color C_REENTRY = color.new(color.teal, 0)
color C_RSHORT  = color.new(color.maroon, 0)
color C_PREVIEW = color.new(color.yellow, 0)
color C_RISK    = color.new(color.orange, 0)
color C_BGLONG  = color.new(color.green, 95)
color C_BGSHORT = color.new(color.red, 95)

if showLabels and (longEntry or RL)
    tag = RL ? 'RL' : 'E'
    label.new(bar_index, low, tag, style = label.style_label_up, color = RL ? C_REENTRY : C_LONG, textcolor = color.white, size = size.small)
if showLabels and (shortEntry or RS)
    tag2 = RS ? 'RS' : 'S'
    label.new(bar_index, high, tag2, style = label.style_label_down, color = RS ? C_RSHORT : C_SHORT, textcolor = color.white, size = size.small)
if showLabels and (longExit or shortExit)
    label.new(bar_index, high, longExit ? 'XL' : 'XS', style = label.style_xcross, color = C_EXIT, textcolor = color.white, size = size.tiny)
if showLabels and riskHit
    label.new(bar_index, longTPHit or shortSLHit ? high : low, longTPHit or shortTPHit ? 'TP' : 'SL', style = label.style_xcross, color = C_RISK, textcolor = color.white, size = size.tiny)
if showLabels and (previewLong or previewShort)
    label.new(bar_index, previewLong ? low : high, 'P', style = previewLong ? label.style_triangleup : label.style_triangledown, color = C_PREVIEW, textcolor = color.black, size = size.tiny)

color bgPosColor = not showBg ? na : posState == 1 ? C_BGLONG : posState == -1 ? C_BGSHORT : na
bgcolor(bgPosColor, title = 'Position Background')

string posText  = posState == 1 ? 'LONG' : posState == -1 ? 'SHORT' : 'FLAT'
color  posColor = posState == 1 ? C_LONG : posState == -1 ? C_SHORT : color.gray
string flowText = tradeDiff >= 0 ? 'ACCUMULATION' : 'DISTRIBUTION'
color  flowColor = tradeDiff >= 0 ? C_LONG : C_SHORT
string htfText  = not useMTF ? 'OFF' : not htfOk ? 'INVALID TF' : htfDir == 1 ? 'BULLISH' : htfDir == -1 ? 'BEARISH' : 'NEUTRAL'
color  htfColor = htfDir == 1 ? C_LONG : htfDir == -1 ? C_SHORT : color.gray
string riskText = na(slPrice) or na(tpPrice) ? 'n/a' : str.tostring(slPrice, format.mintick) + '  /  ' + str.tostring(tpPrice, format.mintick)

var table dash = table.new(position.top_right, 2, 6, bgcolor = color.new(color.black, 60), border_width = 1, border_color = color.new(color.gray, 60))
if showDash and barstate.islast
    table.cell(dash, 0, 0, 'Position', text_color = color.silver, text_size = size.tiny)
    table.cell(dash, 1, 0, posText, text_color = posColor, text_size = size.tiny)
    table.cell(dash, 0, 1, 'Fund Flow', text_color = color.silver, text_size = size.tiny)
    table.cell(dash, 1, 1, flowText, text_color = flowColor, text_size = size.tiny)
    table.cell(dash, 0, 2, 'Buy - Sell', text_color = color.silver, text_size = size.tiny)
    table.cell(dash, 1, 2, str.tostring(tradeDiff, format.mintick), text_color = color.white, text_size = size.tiny)
    table.cell(dash, 0, 3, 'ATR', text_color = color.silver, text_size = size.tiny)
    table.cell(dash, 1, 3, na(atrVal) ? 'n/a' : str.tostring(atrVal, format.mintick), text_color = color.white, text_size = size.tiny)
    table.cell(dash, 0, 4, 'Stop / Target', text_color = color.silver, text_size = size.tiny)
    table.cell(dash, 1, 4, riskText, text_color = color.white, text_size = size.tiny)
    table.cell(dash, 0, 5, 'HTF Gate', text_color = color.silver, text_size = size.tiny)
    table.cell(dash, 1, 5, htfText, text_color = htfColor, text_size = size.tiny)

// -------------------------------------------------------------
// Alerts (global scope; close-confirmed)
// -------------------------------------------------------------
if longEntry or RL
    alert('L1 Banker Fund X: long entry @ ' + str.tostring(close, format.mintick) + ' (' + (RL ? 're-entry' : 'cross') + ')', alert.freq_once_per_bar_close)
if shortEntry or RS
    alert('L1 Banker Fund X: short entry @ ' + str.tostring(close, format.mintick) + ' (' + (RS ? 're-entry' : 'cross') + ')', alert.freq_once_per_bar_close)
if longExit
    alert('L1 Banker Fund X: long exit @ ' + str.tostring(close, format.mintick), alert.freq_once_per_bar_close)
if shortExit
    alert('L1 Banker Fund X: short exit @ ' + str.tostring(close, format.mintick), alert.freq_once_per_bar_close)
if riskHit
    alert('L1 Banker Fund X: risk level hit @ ' + str.tostring(close, format.mintick), alert.freq_once_per_bar_close)
if previewLong or previewShort
    alert('L1 Banker Fund X: UNCONFIRMED preview ' + (previewLong ? 'long' : 'short') + ' @ ' + str.tostring(close, format.mintick), alert.freq_once_per_bar)

alertcondition(longEntry,  'Long Entry',       'L1 Banker Fund X: long entry (Buy Line crossed above Sell Line)')
alertcondition(shortEntry, 'Short Entry',      'L1 Banker Fund X: short entry (Buy Line crossed below Sell Line)')
alertcondition(longExit,   'Long Exit',        'L1 Banker Fund X: long exit')
alertcondition(shortExit,  'Short Exit',       'L1 Banker Fund X: short exit')
alertcondition(RL,         'Long Re-entry',    'L1 Banker Fund X: long re-entry (RL)')
alertcondition(RS,         'Short Re-entry',   'L1 Banker Fund X: short re-entry (RS)')
alertcondition(riskHit,    'TP / SL Hit',      'L1 Banker Fund X: take-profit or stop-loss level reached')
alertcondition(previewLong or previewShort, 'Preview (Unconfirmed)', 'L1 Banker Fund X: unconfirmed intrabar preview signal')
